﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Mail;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using HtmlAgilityPack;

namespace Mime
{
    public class MhtBuilder
    {
        private int inProgress = 0;
        private string  previousUrl = "";
        private HashSet<string> failedUri = new HashSet<string>();

        public void Save(Uri pageUrl, string path, int depth = 0) {
            var doc = new HtmlDocument();
            using (WebDownload web = new WebDownload()) {
                using (var stream = web.OpenRead(pageUrl)) {
                    doc.Load(stream, true);
                }
                pageUrl = web.ResponseUri;
            }
            var baseUri = pageUrl;
            //<BASE HREF='http://www.passiontimes.hk/'>
            var baseNode = doc.DocumentNode.SelectSingleNode("//base");
            if (baseNode != null) {
                baseUri = new Uri(baseNode.GetAttributeValue("href", ""));
            }
            
            var refreshNodes = doc.DocumentNode.SelectNodes("//meta[@http-equiv]");
            if (refreshNodes != null) {
                foreach (var refreshNode in refreshNodes) {
                    if (refreshNode.GetAttributeValue("http-equiv", "").Equals("refresh", StringComparison.InvariantCultureIgnoreCase)) {

                        var refreshContent = refreshNode.GetAttributeValue("content", "");
                        var urlPos = refreshContent.IndexOf("url", 0, StringComparison.InvariantCultureIgnoreCase);
                        if (urlPos >= 0) {
                            var a = refreshContent.IndexOf("=", urlPos);
                            if (a >= 0) {

                                var refreshUrl = refreshContent.Substring(a + 1).Trim();
                                var b = refreshUrl.IndexOf(";");
                                if (b > 0) {
                                    refreshUrl = refreshUrl.Substring(0, b).Trim();
                                }

                                if (previousUrl != refreshUrl) {
                                    previousUrl = refreshUrl;
                                    var refreshUri = ResolveUri(baseUri, refreshUrl);
                                    if (refreshUri != null) {
                                        if (!Configuration.ForbidRefreshDomains.Contains(refreshUri.DnsSafeHost)) {
                                            Save(refreshUri, path, depth + 1);
                                            return;
                                        }
                                    }
                                }
                            }
                        }

                        refreshNode.Remove();
                    }
                }
            }

            var bodyNode = doc.DocumentNode.SelectSingleNode("//body");
            if (bodyNode == null) {
                var descriptionNode = doc.DocumentNode.SelectSingleNode("//meta[@name='description']");
                var ogUrlNode = doc.DocumentNode.SelectSingleNode("//meta[@property='og:url']");
                var anchor = "";
                if (ogUrlNode != null) {
                    anchor = string.Format("<a href='{0}'>More</a>", WebUtility.HtmlDecode(ogUrlNode.GetAttributeValue("content", "")));
                }
                if (descriptionNode != null) {
                    bodyNode = doc.CreateElement("body");
                    bodyNode.InnerHtml = WebUtility.HtmlEncode(descriptionNode.GetAttributeValue("content", "")) + "<br>" + anchor;
                    doc.DocumentNode.AppendChild(bodyNode);
                }
            }

            SusitituteSrc(doc, "pagespeed_lazy_src");
            SusitituteSrc(doc, "data-img-path");

            var map = new Dictionary<string, string>();
            var i = 0;

            i = ProcessNodes(doc, map, i, "src");
            i = ProcessNodes(doc, map, i, "background");

            var nodes = doc.DocumentNode.SelectNodes("//a[@href]");
            if (nodes != null) {
                foreach (var node in nodes) {
                    var href = node.GetAttributeValue("href", "");
                    Uri uri = ResolveUri(baseUri, href);
                    if (uri != null) {
                        node.SetAttributeValue("href", uri.ToString());
                    }
                }
            }

            nodes = doc.DocumentNode.SelectNodes("//link[@rel='stylesheet']");
            if (nodes != null) {
                foreach (var node in nodes) {
                    try {
                        var href = node.GetAttributeValue("href", "");
                        var client = new WebDownload();
                        if (!string.IsNullOrWhiteSpace(href)) {
                            Uri uri = ResolveUri(baseUri, href);
                            if (uri != null) {
                                var style = client.DownloadString(uri);
                                if (!string.IsNullOrWhiteSpace(style)) {
                                    node.Name = "style";
                                    node.InnerHtml = style;
                                }
                                node.SetAttributeValue("href", uri.ToString());
                            }

                        }
                    }
                    catch { }
                }
            }

            nodes = doc.DocumentNode.SelectNodes("//style");
            if (nodes != null) {
                foreach (var node in nodes) {
                    var href = node.GetAttributeValue("href", "");
                    var cssUri = baseUri;
                    if (!string.IsNullOrWhiteSpace(href)) {
                        cssUri = new Uri(href);
                        node.Attributes.Remove("href");
                    }
                    var style = node.InnerHtml;
                    if (!string.IsNullOrWhiteSpace(style)) {
                        style = ProcessStyleUrl(map, style, cssUri);

                        node.InnerHtml = style;
                    }
                }
            }

            nodes = doc.DocumentNode.SelectNodes("//@style");
            if (nodes != null) {
                foreach (var node in nodes) {
                    var style = node.GetAttributeValue("style", "");
                    if (!string.IsNullOrWhiteSpace(style)) {
                        style = ProcessStyleUrl(map, style, baseUri);

                        node.SetAttributeValue("style", style);
                    }
                }
            }

            MailMessage message = new MailMessage("test@example.com", "test@example.com");
            AlternateView view = AlternateView.CreateAlternateViewFromString(doc.DocumentNode.OuterHtml, new System.Net.Mime.ContentType("text/html"));
            view.TransferEncoding = System.Net.Mime.TransferEncoding.QuotedPrintable;
            message.AlternateViews.Add(view);

            foreach (var url in map.Keys) {
                var candidateUrl = url;
                lock (this) {
                    inProgress++;
                }
                ThreadPool.QueueUserWorkItem((a)=> {

                    Uri uri = ResolveUri(baseUri, WebUtility.HtmlDecode(url));
                    if (uri != null) {
                        try {
                            if (uri.Scheme.StartsWith("http") && !failedUri.Contains(uri.ToString())) {
                                WebDownload client = new WebDownload();
                                string filename = map[url];
                                Debug.WriteLine(string.Format("Downloading: {0} ==> {1}", filename, uri));
                                MemoryStream stream = new MemoryStream(client.DownloadData(uri));
                                LinkedResource resource = new LinkedResource(stream);
                                resource.ContentId = filename;

                                view.LinkedResources.Add(resource);
                            }
                            else {
                                SecondaryDownload(baseUri, map, view, url, uri);
                            }
                        }
                        catch {
                            lock (failedUri) {
                                failedUri.Add(uri.ToString());
                            }
                            SecondaryDownload(baseUri, map, view, url, uri);
                        }
                        finally {
                            lock (this) {
                                inProgress--;
                            }
                        }
                    }
                });
            }

            for (; ; ) {
                if (inProgress > 0) {
                    Thread.Sleep(1000);
                }
                else {
                    break;
                }
            }

            var tempDir = Path.Combine(Environment.CurrentDirectory, Guid.NewGuid().ToString("D").Replace("-", ""));
            Directory.CreateDirectory(tempDir);
            SmtpClient smtp = new SmtpClient();
            smtp.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory;
            smtp.PickupDirectoryLocation = tempDir;

            try {
                smtp.Send(message);
                File.Move(Directory.GetFiles(tempDir).First(), path);
            }
            catch (Exception e) {
                if (depth < 3) {
                    Thread.Sleep(30000);
                    Save(pageUrl, path, depth + 1);
                }
                else {
                    Debug.WriteLine(e.Message);
                }
            }
            finally {
                Directory.Delete(tempDir, true);
            }

        }

        private void SecondaryDownload(Uri baseUri, Dictionary<string, string> map, AlternateView view, string url, Uri uri) {
            try {
                Uri nonDecodeUri = ResolveUri(baseUri, url);
                if (nonDecodeUri != null &&
                    !uri.Equals(nonDecodeUri) &&
                    !failedUri.Contains(nonDecodeUri.ToString())) {
                    WebDownload client = new WebDownload();
                    string filename = map[url];
                    Debug.WriteLine(string.Format("Downloading: {0} ==> {1}", filename, nonDecodeUri));
                    try {
                        MemoryStream stream = new MemoryStream(client.DownloadData(nonDecodeUri));
                        LinkedResource resource = new LinkedResource(stream);
                        resource.ContentId = filename;
                        view.LinkedResources.Add(resource);
                    }
                    catch {
                        lock (failedUri) {
                            failedUri.Add(uri.ToString());
                        }
                        throw;
                    }
                }
            }
            catch (Exception e) {
                Debug.WriteLine(e.Message);
            }
        }

        private static void SusitituteSrc(HtmlDocument doc, string attribute) {
            var nodes = doc.DocumentNode.SelectNodes("//@" + attribute);
            if (nodes != null) {
                foreach (var node in nodes) {
                    if (node.Attributes.Contains("src")) {
                        var src = node.GetAttributeValue(attribute, "");
                        node.SetAttributeValue("src", src);
                    }
                }
            }
        }

        private static int ProcessNodes(HtmlDocument doc, Dictionary<string, string> map, int i, string attribute) {
            var nodes = doc.DocumentNode.SelectNodes("//@" + attribute);
            if (nodes != null) {
                foreach (var node in nodes) {
                    var src = node.GetAttributeValue(attribute, "");
                    if (!src.StartsWith("data:") && !src.StartsWith("cid:")) {
                        if (!map.ContainsKey(src)) {
                            var filename = string.Format("{0:00000}", i++);
                            map.Add(src, filename);
                        }
                        node.SetAttributeValue(attribute, "cid:" + map[src]);
                    }
                }
            }
            return i;
        }

        private Uri ResolveUri(Uri source, string href) {
            Uri uri = null;
            href = WebUtility.HtmlDecode(href);
            try {
                if (href.StartsWith("http")) {
                    uri = new Uri(href);
                }
                else {
                    uri = new Uri(source, href);
                }
            }
            catch {
                try {
                    uri = new Uri(source, href);
                }
                catch {
                    return null;
                }
            }
            return uri;
        }

        private string ProcessStyleUrl(Dictionary<string, string> map, string style, Uri baseUri) {
            Regex exp = new Regex(@"url\(\""(.+?)\""|url\(\'(.+?)\'\)|url\((.+?)\)");
            var matches = exp.Matches(style);

            foreach (Match match in matches) {
                var groups = match.Groups.Cast<Group>().Skip(1).ToList();
                foreach (Group group in groups) {
                    if (group.Success) {
                        var src = group.Value;
                        if (!src.StartsWith("data:") && !src.StartsWith("cid:")) {
                            var url = ResolveUri(baseUri, src).ToString();
                            if (url != null && !map.ContainsKey(url)) {
                                //var ext = src.Substring(src.LastIndexOf("."));
                                map.Add(url, string.Format("{0:00000}", Guid.NewGuid().ToString("D")));
                            }
                        }
                    }
                }
            }

            foreach (var key in map.Keys) {
                if (!string.IsNullOrWhiteSpace(key)) {
                    style = style.Replace(key, "cid:" + map[key]);
                }
            }

            return style;
        }
    }
}
